Análisis de crimen en Jalisco

En este documento se pretende analizar y visualizar un conjunto de 267,157 observaciones de crímenes de diferentes categorías ocurridos en los municipios de GUADALAJARA, SAN PEDRO TLAQUEPAQUE, TLAJOMULCO DE ZÚÑIGA, TONALÁ y ZAPOPAN.

Estos datos fueron obtenidos del IEEG con registros desde el 01/01/2017 hasta el 01/01/2025.

Los pasos que seguiremos serán los siguientes:

  1. Cargar librerías
  2. Carga de datos
  3. Categorización de variables
  4. Limpieza de datos
  5. Visualización de datos
  6. Implementación de un modelo predictivo
  7. Conclusiones

Uso de librerías

En el bloque siguiente se cargarán las librerías necesarias para llevar a cabo el ejercicio.

# install.packages("tidyverse") # Lectura csv y manejo de strings
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.2     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
# install.packages("readr") # Función parse_time para las fechas
library(readr)
# install.packages("dplyr") # Funciones para manipulación de datos
library(dplyr)
# install.packages("missForest") # Imputación de datos faltantes
library(missForest)
# install.packages("hms") # Conversor de segundos a horas
library(hms)
## 
## Adjuntando el paquete: 'hms'
## 
## The following object is masked from 'package:lubridate':
## 
##     hms
# install.packages("lubridate") # Manejo de fechas
library(lubridate)
# install.packages("xgboost") # Modelos de machine learning
library(xgboost) 
## 
## Adjuntando el paquete: 'xgboost'
## 
## The following object is masked from 'package:dplyr':
## 
##     slice
# install.packages("ggplot2") # Visualización de datos
library(ggplot2)
# install.packages("leaflet") # Mapas interactivos
library(leaflet)
# install.packages("sf") # Manejo de datos espaciales
library(sf)
## Linking to GEOS 3.13.1, GDAL 3.10.2, PROJ 9.5.1; sf_use_s2() is TRUE
# install.packages("randomForest") # Modelos de random forest
library(randomForest)
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
## 
## Adjuntando el paquete: 'randomForest'
## 
## The following object is masked from 'package:dplyr':
## 
##     combine
## 
## The following object is masked from 'package:ggplot2':
## 
##     margin
# install.packages("caret") # Utilidades para machine learning
library(caret)
## Cargando paquete requerido: lattice
## 
## Adjuntando el paquete: 'caret'
## 
## The following object is masked from 'package:purrr':
## 
##     lift
# install.packages("stringr") # Manipulación de cadenas
library(stringr)
# install.packages("leafgl") # Visualización eficiente en leaflet
library(leafgl)

Carga de datos

Una vez descargado el archivo de la Plataforma de Seguridad IEEG, y colocado en nuestro directorio de trabajo, procedemos a cargarlo.

data <- read_csv(
  "delitos.csv", # Nombre del archivo
  na = c("NA", "N.D.", "NO DISPONIBLE"), # Cadenas que se trataran como valores NULL
  show_col_types = FALSE # No se mostrara el resultado en consola.
)
summary(data) # Muestra un resumen de los datos.
##      fecha               delito                x                y        
##  Min.   :2017-01-01   Length:267157      Min.   :-103.6   Min.   :20.39  
##  1st Qu.:2018-07-27   Class :character   1st Qu.:-103.4   1st Qu.:20.61  
##  Median :2020-06-01   Mode  :character   Median :-103.4   Median :20.65  
##  Mean   :2020-08-31                      Mean   :-103.4   Mean   :20.65  
##  3rd Qu.:2022-10-10                      3rd Qu.:-103.3   3rd Qu.:20.69  
##  Max.   :2025-01-01                      Max.   :-103.1   Max.   :20.96  
##                                          NA's   :13008    NA's   :13008  
##    colonia           municipio           clave_mun         hora          
##  Length:267157      Length:267157      Min.   : 39.0   Length:267157     
##  Class :character   Class :character   1st Qu.: 39.0   Class :character  
##  Mode  :character   Mode  :character   Median : 97.0   Mode  :character  
##                                        Mean   : 81.5                     
##                                        3rd Qu.:101.0                     
##                                        Max.   :120.0                     
##                                                                          
##  bien_afectado      zona_geografica   
##  Length:267157      Length:267157     
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

Categorización y ajuste de variables

Se puede observar en el resumen de datos que hora es de formato character, cuando este debe ser de tipo tiempo o numérico, se verificó la columna y se encontraron celdas con terminación PM, AM, y espacios en blanco y se procede a su limpieza y acomodo.
Para después, convertirlos en valores de tipo numérico.
También se realizará una factorización de las variables a categorías, para eficientizar la memoria y evitar la repetición de datos así como a ayudar a modelos predictivos.

# Correccion de hora
data <- data %>%
  mutate(
    # Transformacion de valores
    hora = hora %>% # Se asiga a la misma columna
      str_trim() %>% # Eliminamos espacios vacios.
      str_replace_all(";", ":") %>% # Quitamos valores espesificos
      str_replace_all("O", "0") %>% # Corregimos errores de registo
      str_remove_all(",") %>%
      str_remove_all("\\s*(a\\.m\\.|p\\.m\\.)") %>% # Limpiamos los valores am y pm
      str_extract("\\d{1,2}:\\d{2}"),
    # Damos un formato adecuado
  )

# Convertimos a formatos acorde al tipo de dato.
data <- data %>%
  mutate(
    fecha = as.Date(fecha),
    hora = parse_time(hora, "%H:%M")
  )

# Factorizacion de las columnas
data <- data %>%
  mutate(across(
    c(delito, colonia, municipio, clave_mun, bien_afectado), # Columnas a convertie en factor.
    as.factor
  ))
summary(data) # Verificamos los datos.
##      fecha                                      delito            x         
##  Min.   :2017-01-01   Robo a vehiculos particulares:85220   Min.   :-103.6  
##  1st Qu.:2018-07-27   Violencia familiar           :67459   1st Qu.:-103.4  
##  Median :2020-06-01   Lesiones dolosas             :41702   Median :-103.4  
##  Mean   :2020-08-31   Robo casa habitacion         :27439   Mean   :-103.4  
##  3rd Qu.:2022-10-10   Robo de motocicleta          :16556   3rd Qu.:-103.3  
##  Max.   :2025-01-01   Abuso sexual infantil        :16269   Max.   :-103.1  
##                       (Other)                      :12512   NA's   :13008   
##        y                      colonia                       municipio    
##  Min.   :20.39   ZONA CENTRO      :  3450   GUADALAJARA          :99296  
##  1st Qu.:20.61   CENTRO           :  2847   SAN PEDRO TLAQUEPAQUE:37232  
##  Median :20.65   HACIENDA SANTA FE:  2417   TLAJOMULCO DE ZUÑIGA :37076  
##  Mean   :20.65   OBLATOS          :  2412   TONALA               :29976  
##  3rd Qu.:20.69   CHULAVISTA E1    :  2230   ZAPOPAN              :63577  
##  Max.   :20.96   (Other)          :237280                                
##  NA's   :13008   NA's             : 16521                                
##  clave_mun        hora                
##  39 :99296   Min.   :00:00:00.000000  
##  97 :37076   1st Qu.:07:05:00.000000  
##  98 :37232   Median :14:30:00.000000  
##  101:29976   Mean   :13:20:52.774186  
##  120:63577   3rd Qu.:18:25:00.000000  
##              Max.   :23:59:00.000000  
##              NA's   :75544            
##                            bien_afectado    zona_geografica   
##  El patrimonio                    :129215   Length:267157     
##  La familia                       : 67459   Class :character  
##  La libertad y la seguridad sexual: 19045   Mode  :character  
##  La vida y la integridad corporal : 51438                     
##                                                               
##                                                               
## 

Limpieza de los datos

Debido a que estos datos se encuentran en una zona geográfica específica ZMG (Zona Metropolitana de Guadalajara), es una columna que nos es irrelevante pues todos los datos la tienen, así que para reducir datos procederemos a la limpieza de esa columna así como a la limpieza de los datos con NA correlacionados, por ejemplo:

Tenemos datos que no tienen colonia ni coordenada, si bien pudiéramos dar una solución, posiblemente mediante un modelo de imputación como missForest, que nos ubique los delitos vacíos en la colonia más probable, no es el fin de este ejemplo, así que se decide suprimirlos.

Pero para fines de que el lector vea una implementación de missForest, se hará una media con la hora, para eliminar los valores NULL.

# Apartir de aqui, crearemos cambiaremos la variable data, para dejar al lector, la posibilidad de implementar el modelo de predicion de colonia con los datos originales.

# Extraer observaciones, donde no hay colonia y tampoco cordenada x e y.
cleanData <- data %>%
  filter(!is.na(colonia) & !is.na(x) & !is.na(y))

# Extraemos la columna zona geografica
cleanData <- cleanData %>% select(-zona_geografica)

# Imputacion de la hora con missForest
imp <- cleanData %>% filter(!is.na(hora)) %>% # Quitamos los valores NA
  mutate(hora_seg = as.numeric(hora)) %>% # cambiamos el formato de la hora, para ayudar al algoritmo
  select(hora_seg, delito, municipio, bien_afectado) # Extraemos las columnas para el entrenamiento.

set.seed(123) # Para que el leector pueda replicar los datos.

model <- missForest(imp)
## Warning in mean.default(xmis[, t.co], na.rm = TRUE): argument is not numeric or
## logical: returning NA
# Reemplaza en tu dataset original
cleanData$hora <- ifelse(is.na(cleanData$hora),
                         as_hms(model$ximp$hora_seg),
                         cleanData$hora)

cleanData$hora <- as_hms(cleanData$hora)

summary(cleanData) # verificamos
##      fecha                                      delito            x         
##  Min.   :2017-01-01   Robo a vehiculos particulares:82498   Min.   :-103.6  
##  1st Qu.:2018-08-16   Violencia familiar           :63044   1st Qu.:-103.4  
##  Median :2020-06-23   Lesiones dolosas             :39256   Median :-103.4  
##  Mean   :2020-09-12   Robo casa habitacion         :25915   Mean   :-103.4  
##  3rd Qu.:2022-10-09   Robo de motocicleta          :15925   3rd Qu.:-103.3  
##  Max.   :2025-01-01   Abuso sexual infantil        :12532   Max.   :-103.1  
##                       (Other)                      :11466                   
##        y                      colonia                       municipio    
##  Min.   :20.40   ZONA CENTRO      :  3450   GUADALAJARA          :96148  
##  1st Qu.:20.61   CENTRO           :  2847   SAN PEDRO TLAQUEPAQUE:35825  
##  Median :20.65   HACIENDA SANTA FE:  2417   TLAJOMULCO DE ZUÑIGA :31257  
##  Mean   :20.65   OBLATOS          :  2412   TONALA               :27706  
##  3rd Qu.:20.69   CHULAVISTA E1    :  2230   ZAPOPAN              :59700  
##  Max.   :20.86   SAN ANDRES       :  1655                                
##                  (Other)          :235625                                
##  clave_mun        hora                
##  39 :96148   Min.   :00:00:00.000000  
##  97 :31257   1st Qu.:07:30:00.000000  
##  98 :35825   Median :14:00:00.000000  
##  101:27706   Mean   :13:21:28.295536  
##  120:59700   3rd Qu.:18:40:00.000000  
##              Max.   :23:59:00.000000  
##                                       
##                            bien_afectado   
##  El patrimonio                    :124338  
##  La familia                       : 63044  
##  La libertad y la seguridad sexual: 14863  
##  La vida y la integridad corporal : 48391  
##                                            
##                                            
## 

Propuesta para modelo predictivo para colonia

Se propone el siguiente modelo de randomForest para predecir la colonia a la que puede pertenecer un delito sin colonia registrada, usando las variables limpias y procesadas en los pasos anteriores. Primero, se fija una semilla aleatoria para asegurar la reproducibilidad del modelo. Se entrena el modelo con 100 árboles usando todos los predictores disponibles en cleanData para predecir la variable colonia. Después, se selecciona aleatoriamente un 20% de los datos para evaluar el desempeño del modelo, realizando una predicción sobre esta muestra de prueba. Se calcula la precisión del modelo como la proporción de predicciones correctas en el conjunto de test y se imprime este resultado en porcentaje. Finalmente, el modelo entrenado se guarda en dos formatos diferentes para su posterior uso o análisis, y se muestra cómo cargar el modelo guardado desde un archivo.

# Entrenamiento de modelo randomForest para predecir colonia
# set.seed(123)
# modelo_colonia <- randomForest(colonia ~ .,
                              #  data = cleanData,
                              #  ntree = 100,
                              #  importance = TRUE)
# Leer modelo
modelo_colonia <- readRDS("Modelo_Predictor_de_Colonia.rds")

# Selección de muestra de test
set.seed(42)
indice_test <- sample(1:nrow(cleanData), size = 0.2 * nrow(cleanData))
data_test_sample <- cleanData[indice_test, ]

# Predicción con el modelo ya entrenado
pred_test <- predict(modelo_colonia, newdata = data_test_sample)

# Calcular precisión
accuracy <- mean(pred_test == data_test_sample$colonia)
print(paste("Precisión de muestra de test:", round(accuracy * 100, 2), "%"))
## [1] "Precisión de muestra de test: 67.38 %"
# Guardar modelo en un archivo
saveRDS(modelo_colonia, "Modelo_Predictor_de_Colonia.rds")
save(modelo_colonia, file = "Modelo_Predictor_de_Colonia.RData")

Visualización de datos

Una vez que limpiamos los datos, pasemos a ver algunas gráficas interesantes:

  1. Violencia por colonia
cleanData %>%
  filter(!is.na(colonia)) %>%
  count(colonia, sort = TRUE) %>%
  slice_max(n, n = 20) %>%
  ggplot(aes(x = reorder(colonia, n), y = n)) +
  geom_col(fill = "firebrick") +
  coord_flip() +
  labs(title = "Colonias más violentas", x = "Colonia", y = "Número de delitos")

  1. Ubicaciones de los crímenes en el mapa
pal <- colorFactor(palette = "Set1", domain = cleanData$delito)

# Crear el mapa
cleanData %>%
  filter(!is.na(x) & !is.na(y)) %>%
  leaflet() %>%
  addTiles() %>%
  addCircles(
    lng = ~ x,
    lat = ~ y,
    radius = 20,
    color = ~ pal(delito),
    stroke = FALSE,
    fillOpacity = 0.5,
    label = ~ delito
  ) %>%
  addLegend(
    "bottomright",
    pal = pal,
    values = ~ delito,
    title = "Tipo de Delito"
  )
  1. Crímenes más comunes por municipio.
cleanData %>%
  filter(!is.na(municipio)) %>%
  count(municipio, delito, sort = TRUE) %>%
  group_by(municipio) %>%
  slice_max(n, n = 20) %>%
  ggplot(aes(
    x = reorder(municipio, n),
    y = n,
    fill = delito
  )) +
  geom_col() +
  coord_flip() +
  labs(title = "Crímenes más comunes por municipio", x = "Municipio", y = "Número de delitos")

  1. Horas más peligrosas por municipio
cleanData %>%
  filter(!is.na(hora) & !is.na(colonia)) %>%
  group_by(colonia, hora) %>%
  summarise(n = n(), .groups = "drop") %>%
  group_by(colonia) %>%
  slice_max(n, n = 3) %>%
  arrange(desc(n)) %>%
  head(10) %>%
  ggplot(aes(x = reorder(colonia, n), y = n, fill = as.character(hora))) +
  geom_col() +
  coord_flip() +
  labs(
    title = "Hora más peligrosa por colonia",
    x = "Colonia",
    y = "Número de delitos",
    fill = "Hora"
  ) +
  scale_fill_viridis_d()

Implementación de un modelo

Modelo predictivo que, dada una colonia, municipio, hora y año, prediga el crimen más probable a suceder.

# Filtrar columnas relevantes
cleanData <- cleanData %>%
  mutate(anio = year(fecha)) %>% mutate(across(c(anio), as.factor))

modelo_data <- cleanData %>%
  select(delito, municipio, colonia, hora, anio)

# Codificar variable objetivo (delito) como factor y extraer niveles
modelo_data$delito <- as.factor(modelo_data$delito)
labels <- modelo_data$delito
label_levels <- levels(labels)
modelo_data$delito <- as.integer(labels) - 1

modelo_data$anio <- as.factor(modelo_data$anio)

# Generar variables dummies
dummies <- dummyVars(~ municipio + colonia + hora + anio, data = modelo_data)
data_matrix <- predict(dummies, newdata = modelo_data)
data_matrix <- as.matrix(data_matrix)

set.seed(123)
train_idx <- createDataPartition(modelo_data$delito, p = 0.8, list = FALSE)
train_x <- data_matrix[train_idx, ]
train_y <- modelo_data$delito[train_idx]
test_x <- data_matrix[-train_idx, ]
test_y <- modelo_data$delito[-train_idx]


xgb_model <- xgboost(
  data = train_x,
  label = train_y,
  nrounds = 100,
  objective = "multi:softprob",
  num_class = length(label_levels),
  eval_metric = "mlogloss",
  verbose = 0
)

pred_probs <- predict(xgb_model, newdata = test_x)
pred_matrix <- matrix(pred_probs, nrow = length(label_levels), byrow = TRUE)
pred_labels <- max.col(t(pred_matrix)) - 1

# Matriz de confusión
confusionMatrix(factor(pred_labels, levels = 0:(length(label_levels) - 1)),
                factor(test_y, levels = 0:(length(label_levels) - 1)),
                mode = "everything")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    0    1    2    3    4    5    6    7    8
##          0  224    2  145  651 1382  428  287   40 1170
##          1  287    3  189  822 1869  610  348   52 1415
##          2  237    3  174  765 1698  510  308   59 1285
##          3  204    4  149  664 1462  462  271   37 1076
##          4  179    3  145  619 1535  458  271   43 1058
##          5  215    3  178  860 1645  444  301   54 1125
##          6  371    3  265 1192 2478  763  455   74 1875
##          7  372    3  255 1172 2449  743  458   72 1783
##          8  391    4  227  981 2205  799  443   55 1811
## 
## Overall Statistics
##                                           
##                Accuracy : 0.1074          
##                  95% CI : (0.1047, 0.1101)
##     No Information Rate : 0.3336          
##     P-Value [Acc > NIR] : 1               
##                                           
##                   Kappa : 0.0024          
##                                           
##  Mcnemar's Test P-Value : <2e-16          
## 
## Statistics by Class:
## 
##                      Class: 0  Class: 1 Class: 2 Class: 3 Class: 4 Class: 5
## Sensitivity          0.090323 1.071e-01 0.100753  0.08594  0.09179 0.085106
## Specificity          0.913846 8.884e-01 0.899483  0.91356  0.91690 0.902449
## Pos Pred Value       0.051744 5.362e-04 0.034531  0.15338  0.35607 0.092021
## Neg Pred Value       0.950740 9.994e-01 0.965556  0.84580  0.66850 0.894640
## Precision            0.051744 5.362e-04 0.034531  0.15338  0.35607 0.092021
## Recall               0.090323 1.071e-01 0.100753  0.08594  0.09179 0.085106
## F1                   0.065795 1.067e-03 0.051434  0.11016  0.14595 0.088429
## Prevalence           0.049474 5.586e-04 0.034452  0.15413  0.33361 0.104076
## Detection Rate       0.004469 5.985e-05 0.003471  0.01325  0.03062 0.008858
## Detection Prevalence 0.086361 1.116e-01 0.100525  0.08636  0.08600 0.096256
## Balanced Accuracy    0.502084 4.978e-01 0.500118  0.49975  0.50434 0.493778
##                      Class: 6 Class: 7 Class: 8
## Sensitivity          0.144812 0.148148  0.14375
## Specificity          0.850569 0.854254  0.86397
## Pos Pred Value       0.060861 0.009854  0.26186
## Neg Pred Value       0.937000 0.990332  0.75036
## Precision            0.060861 0.009854  0.26186
## Recall               0.144812 0.148148  0.14375
## F1                   0.085704 0.018478  0.18561
## Prevalence           0.062681 0.009695  0.25132
## Detection Rate       0.009077 0.001436  0.03613
## Detection Prevalence 0.149141 0.145770  0.13797
## Balanced Accuracy    0.497691 0.501201  0.50386
predecir_delito_xgb <- function(mun, col, hora, anio) {
  new_data <- tibble(
    municipio = mun,
    colonia = col,
    hora = hora,
    anio = as.factor(anio)
  )
  new_matrix <- predict(dummies, newdata = new_data)
  new_matrix <- as.matrix(new_matrix)
  probs <- predict(xgb_model, newdata = new_matrix)
  pred_class <- which.max(probs) - 1
  label_levels[pred_class + 1]
}

# Ejemplo de uso
predecir_delito_xgb("TLAJOMULCO DE ZUÑIGA", "LOMAS DEL MIRADOR", 14, 2023)
## [1] "Violencia familiar"
predecir_delito_xgb("GUADALAJARA", "MIRAVALLE", 24, 2025)
## [1] "Robo a vehiculos particulares"
predecir_delito_xgb("TONALA", "FRANCISCO VILLA", 12, 2021)
## [1] "Violencia familiar"
predecir_delito_xgb("SAN PEDRO TLAQUEPAQUE", "LAS JUNTAS", 18, 2023)
## [1] "Violencia familiar"
predecir_delito_xgb("ZAPOPAN", "PLAZA GUADALUPE", 23, 2025)
## [1] "Violencia familiar"
predecir_delito_xgb("GUADALAJARA", "8 DE JULIO", 5, 2024)
## [1] "Violencia familiar"
predecir_delito_xgb("TLAJOMULCO DE ZUÑIGA", "CENTRO", 3, 2025)
## [1] "Violencia familiar"
predecir_delito_xgb("ZAPOPAN", "GUADALUPE", 11, 2022)
## [1] "Violencia familiar"
predecir_delito_xgb("GUADALAJARA", "SAN JUAN BOSCO", 16, 2024)
## [1] "Violencia familiar"
predecir_delito_xgb("SAN PEDRO TLAQUEPAQUE", "PAISAJES DEL TESORO", 20, 2023)
## [1] "Violencia familiar"

Conclusiones

No se pudo obtener un modelo que funcione correctamente para predecir datos debido a que por la naturaleza de los mismos, los se encuentran muy sesgados hacia delitos muy comunes, como es el robo de autos y violencia familiar.